feat(apollo-vertex): Roadmap Status page with live Jira data - #962
feat(apollo-vertex): Roadmap Status page with live Jira data#962hfrancis31 wants to merge 1 commit into
Conversation
Dependency License Review
License distribution
Excluded packages
|
There was a problem hiding this comment.
Pull request overview
Adds a new Apollo Vertex documentation page that surfaces “Roadmap status” using live data from the VS Horizontal UX Jira board, including basic card processing (sectioning, badges, epic display) and delivered-link resolution rules.
Changes:
- Added Jira integration utilities to query issues and extract text from Jira ADF descriptions.
- Added issue processing + delivered-link resolution (explicit
Vertex:link, convention-based slug lookup, Jira fallback). - Added a new
/design-system-statuspage (and sidebar entry) rendering a three-section status board, plus an/api/jiraendpoint.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/apollo-vertex/lib/jira.ts | Fetches Jira issues via JQL with pagination; includes ADF text extraction helper. |
| apps/apollo-vertex/lib/jira-resolve.ts | Transforms raw Jira issues into board sections/cards and resolves delivered links. |
| apps/apollo-vertex/app/design-system-status/page.mdx | Adds the new “Roadmap status” MDX page that mounts the status board. |
| apps/apollo-vertex/app/design-system-status/_components/status-board.tsx | Implements the status board UI, error state, and server-side data fetch. |
| apps/apollo-vertex/app/api/jira/route.ts | Adds a JSON API endpoint that returns the processed board data. |
| apps/apollo-vertex/app/_meta.ts | Adds “Roadmap status” to the sidebar under Introduction. |
📊 Coverage + size by packagePer-package bundle size on this PR (no JS/TS source changes detected under
"Coverage" is each package's own |
0xr3ngar
left a comment
There was a problem hiding this comment.
left some comments, address them with Claude, if you need my help lmk.
Also CI checks are failing, please fix before re-requesting review.
There was a problem hiding this comment.
should this even be in the apollo-vertex? shouldn't we just mock jira tickets in the UI and explicitly tell the consumers to connect it to the actual JIRA API. I don't think this needs to be here
There was a problem hiding this comment.
I discussed with @ruudandriessen that this is needed so that we can provide visibility to all eng teams on what is coming and the status. We are wanting more visibility across teams.
There was a problem hiding this comment.
instead of hand rolling our own jira processing can't we just use a sdk npm package ? like
There was a problem hiding this comment.
Thanks for the suggestion. I looked into it. The main reasons I'm keeping the raw fetch approach here:
- Single endpoint: we only call POST /rest/api/3/search/jql. Adding a full SDK to wrap one request adds dependency weight without meaningful simplification.
- v3 cursor pagination: we use nextPageToken, which is specific to the REST v3 API. Most Jira JS SDKs still target v2 (startAt/maxResults) and would require workarounds.
- Intentionally narrow types: JiraIssue only types the fields we actually fetch. SDK-generated types cover the full response shape, which adds noise for no gain here.
Happy to revisit if the integration scope grows, but for a single read-only JQL query this felt like the right trade-off.
There was a problem hiding this comment.
lets split this file up in smaller functions that each handle 1 thing on their own.
| function toSection(statusName: string): Section { | ||
| const s = statusName.toLowerCase(); | ||
| if (s === "closed" || s === "done") return "delivered"; | ||
| if (s === "in progress" || s === "in review" || s === "review") | ||
| return "coming-soon"; | ||
| return "backlog"; | ||
| } |
There was a problem hiding this comment.
I would use a record
const SECTION_BY_STATUS = {
closed: "delivered",
done: "delivered",
"in progress": "coming-soon",
"in review": "coming-soon",
review: "coming-soon",
} satisfies Record<string, Section>;
function toSection(statusName: string): Section {
return SECTION_BY_STATUS[statusName.trim().toLowerCase()] ?? "backlog";
}| function toBadge(labels: string[]): BadgeLabel { | ||
| if (labels.includes("ai-legal-required")) return "required"; | ||
| if (labels.includes("ai-legal-best-practice")) return "best-practice"; | ||
| return null; | ||
| } |
There was a problem hiding this comment.
this would be cleaner if it's
const BADGE_BY_LABEL = {
"ai-legal-required": "required",
"ai-legal-best-practice": "best-practice",
} as const satisfies Record<string, Exclude<BadgeLabel, null>>;
const BADGE_LABEL_PRIORITY = [
"ai-legal-required",
"ai-legal-best-practice",
] as const;
function toBadge(labels: readonly string[]): BadgeLabel {
const label = BADGE_LABEL_PRIORITY.find((candidate) =>
labels.includes(candidate),
);
return label ? BADGE_BY_LABEL[label] : null;
}| function resolveDeliveredLink( | ||
| issue: JiraIssue, | ||
| jiraUrl: string, | ||
| ): { url: string; source: LinkSource } { | ||
| const descText = extractAdfText(issue.fields.description); | ||
|
|
||
| // Priority 1: explicit "Vertex: https://..." or "Vertex URL: https://..." in description | ||
| const vertexMatch = descText.match( | ||
| /Vertex(?:\s+URL)?:\s*(https?:\/\/[^\s\n)]+)/i, | ||
| ); | ||
| if (vertexMatch) { | ||
| return { url: vertexMatch[1].trim(), source: "explicit" }; | ||
| } | ||
|
|
||
| // Priority 2: "Component: <name>" convention — slugify and look up known paths | ||
| const componentMatch = descText.match(/^Component:\s*(.+)$/im); | ||
| if (componentMatch) { | ||
| const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1]; | ||
| if (rawSlug) { | ||
| const slug = rawSlug | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, "-") | ||
| .replace(/(^-|-$)/g, ""); | ||
| const path = SLUG_PATH_MAP.get(slug); | ||
| if (path) return { url: path, source: "convention" }; | ||
| } | ||
| } | ||
|
|
||
| // Priority 3: fall back to Jira ticket | ||
| return { url: jiraUrl, source: "jira" }; | ||
| } |
There was a problem hiding this comment.
all of this regex processing starts making me question if we should really process the description at all
There was a problem hiding this comment.
The best version, is to avoid parsing the description entirely. Jira should ideally expose these as structured custom fields
issue.fields.vertexUrl
issue.fields.componentNameThen the function shrinks to
function resolveDeliveredLink(
issue: JiraIssue,
jiraUrl: string,
): { url: string; source: LinkSource } {
if (issue.fields.vertexUrl) {
return {
url: issue.fields.vertexUrl,
source: "explicit",
};
}
const path = issue.fields.componentName
? PATH_BY_COMPONENT[issue.fields.componentName]
: undefined;
return path
? { url: path, source: "convention" }
: { url: jiraUrl, source: "jira" };
}There was a problem hiding this comment.
For now, keeping the description-based Vertex: convention as a pragmatic workaround to get the board live and visible. Since we don't have this available right now, I'm working with an admin to create one. Once it's available, I'll update lib/jira.ts to fetch the field directly and simplify the parsing in jira-resolve.ts.
| export function processIssues( | ||
| issues: JiraIssue[], | ||
| jiraBaseUrl: string, | ||
| ): BoardData { | ||
| const delivered: ProcessedCard[] = []; | ||
| const comingSoon: ProcessedCard[] = []; | ||
| const backlog: ProcessedCard[] = []; | ||
| const linkStats = { explicit: 0, convention: 0, jiraFallback: 0 }; | ||
|
|
||
| for (const issue of issues) { | ||
| const statusName = issue.fields.status.name; | ||
| const section = toSection(statusName); | ||
| const jiraUrl = `${jiraBaseUrl}/browse/${issue.key}`; | ||
|
|
||
| let link: string; | ||
| let linkSource: LinkSource; | ||
|
|
||
| if (section === "delivered") { | ||
| const resolved = resolveDeliveredLink(issue, jiraUrl); | ||
| link = resolved.url; | ||
| linkSource = resolved.source; | ||
| if (linkSource === "explicit") linkStats.explicit++; | ||
| else if (linkSource === "convention") linkStats.convention++; | ||
| else linkStats.jiraFallback++; | ||
| } else { | ||
| link = jiraUrl; | ||
| linkSource = "jira"; | ||
| } | ||
|
|
||
| const parentIsEpic = | ||
| issue.fields.parent?.fields.issuetype.name === "Epic" || | ||
| issue.fields.parent?.fields.issuetype.hierarchyLevel === 1; | ||
|
|
||
| const card: ProcessedCard = { | ||
| key: issue.key, | ||
| summary: issue.fields.summary, | ||
| status: statusName, | ||
| section, | ||
| badge: toBadge(issue.fields.labels), | ||
| link, | ||
| linkSource, | ||
| jiraUrl, | ||
| updated: issue.fields.updated, | ||
| epicName: parentIsEpic | ||
| ? (issue.fields.parent?.fields.summary ?? null) | ||
| : null, | ||
| epicKey: parentIsEpic ? (issue.fields.parent?.key ?? null) : null, | ||
| }; | ||
|
|
||
| if (section === "delivered") { | ||
| delivered.push(card); | ||
| } else if (section === "coming-soon") { | ||
| // Review/In Review sorts before In Progress — closer to landing | ||
| const sl = statusName.toLowerCase(); | ||
| if (sl === "in review" || sl === "review") comingSoon.unshift(card); | ||
| else comingSoon.push(card); | ||
| } else { | ||
| backlog.push(card); | ||
| } | ||
| } | ||
|
|
||
| return { delivered, comingSoon, backlog, linkStats }; | ||
| } |
There was a problem hiding this comment.
this does way to many things in one place, this let link mutability is a smell. Tell your claude to extract this into seperate small functions so processIssues becomes an orchestrator
0xr3ngar
left a comment
There was a problem hiding this comment.
I accidentally approved oops
35290ac to
60a5584
Compare
|
Apollo Coded App preview deployments are running.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (6)
apps/apollo-vertex/app/api/jira/route.ts:8
- No code in
apps/apollo-vertexappears to call/api/jira(searching for/api/jirareturns no matches), and the Roadmap Status page fetches Jira directly viafetchJiraIssues(). If this endpoint isn’t needed for external consumers, removing it would reduce public attack surface and maintenance burden; otherwise, consider wiring the page to use it so there’s a single data path to maintain.
// Intentionally public — this endpoint surfaces cross-team design system status.
// No auth guard is by design; confirmed with @ruudandriessen.
export async function GET(_req: NextRequest) {
try {
apps/apollo-vertex/lib/jira-resolve.ts:166
- The “Component:” convention only uses the first whitespace-delimited token (e.g., "Alert Dialog" becomes "alert"), which prevents slug resolution for multi-word components like
alert-dialogandbutton-group. Consider slugifying the full component name (optionally trimming any trailing "(…)" or ", …" qualifiers) before looking up the path.
const componentMatch = descText.match(/^Component:\s*(.+)$/im);
if (componentMatch) {
const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
if (rawSlug) {
const slug = rawSlug
apps/apollo-vertex/lib/jira.ts:56
JIRA_BASE_URLmay be configured with a trailing slash (e.g.,https://…/), which would generate a double-slash in the request URL (//rest/api/...). Normalizing the base URL avoids subtle request/redirect issues.
const base = process.env.JIRA_BASE_URL;
apps/apollo-vertex/lib/jira.ts:63
- The missing-config error message tells people to add vars to
.env.local, but the UI error state points toapps/apollo-vertex/.env.local. Aligning the message avoids confusion when setting this up locally.
throw new Error(
"Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
);
apps/apollo-vertex/lib/jira.ts:92
- Throwing the full Jira error response body can leak unnecessary details to downstream callers (and can be very large if Jira returns HTML). Prefer using the status + statusText (and optionally logging the full body server-side) instead of embedding the entire body in the thrown message.
if (!res.ok) {
const text = await res.text();
throw new Error(`Jira API error ${res.status}: ${text}`);
}
apps/apollo-vertex/app/api/jira/route.ts:19
- This route is marked intentionally public and calls Jira using server-side credentials. Two concerns: (1)
Cache-Control: no-storemeans every request triggers a Jira API call, which is vulnerable to accidental/intentional traffic spikes and Jira rate limits; (2) returning the raw exception message can leak internal details. Consider adding CDN caching and only returning a safe error message (still preserving the missing-config hint if you want).
headers: { "Cache-Control": "no-store" },
});
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return NextResponse.json({ error: message }, { status: 500 });
Storybook visual diff⏭️ Skipped: the apollo-design preview deployment did not succeed, so no comparison ran. Logs Updated (PT): Aug 05, 2026, 08:05:34 AM |
60a5584 to
e3f25bf
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
apps/apollo-vertex/lib/jira.ts:94
- The error thrown on non-2xx Jira responses includes the full response body (
await res.text()). This can leak unexpected internal details to the UI/API consumer and can also create extremely large error messages if Jira returns HTML or verbose JSON. Consider only including the status code in production, optionally appending the body only in non-production for debugging.
if (!res.ok) {
// eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
const text = await res.text();
throw new Error(`Jira API error ${res.status}: ${text}`);
}
apps/apollo-vertex/app/api/jira/route.ts:19
- This public API route returns the raw caught error message to clients. In production that can inadvertently expose internal details (for example, upstream HTML/JSON error bodies) and makes the endpoint easier to probe. Consider returning a generic message in production (while still preserving the "Missing Jira configuration" setup hint).
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return NextResponse.json({ error: message }, { status: 500 });
}
apps/apollo-vertex/lib/jira-resolve.ts:215
sortComingSoononly distinguishes review vs non-review and otherwise returns 0, which relies on the JS engine's sort stability to preserve the incoming (JQL) ordering. Adding an explicit tie-breaker (for exampleupdateddescending) avoids potentially non-deterministic ordering and keeps the "most recently updated" items at the top within each group.
function sortComingSoon(cards: ProcessedCard[]): ProcessedCard[] {
return [...cards].sort((a, b) => {
const aReview = isReviewStatus(a.status);
const bReview = isReviewStatus(b.status);
if (aReview !== bReview) return aReview ? -1 : 1;
return 0;
});
}
e3f25bf to
655b054
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
apps/apollo-vertex/app/api/jira/route.ts:15
- This route is public and triggers live Jira fetches, but there are no in-repo references to
/api/jira(it isn’t used by the new StatusBoard either). If it’s not intended for external consumers, consider removing it to reduce public surface area and Jira load, or switch the UI to consume this endpoint so it has a single fetch path.
// Intentionally public — this endpoint surfaces cross-team design system status.
// No auth guard is by design; confirmed with @ruudandriessen.
export async function GET(_req: NextRequest) {
try {
const issues = await fetchJiraIssues();
const jiraBaseUrl =
process.env.JIRA_BASE_URL ?? "https://uipath.atlassian.net";
const data = processIssues(issues, jiraBaseUrl);
return NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
apps/apollo-vertex/lib/jira.ts:64
JIRA_BASE_URLis used as-is. If it’s configured with a trailing slash (common in env files), requests will go to...//rest/api/.... Also, the thrown setup error points to.env.localwhile the UI error state points toapps/apollo-vertex/.env.local, which is inconsistent for users.
const base = process.env.JIRA_BASE_URL;
const email = process.env.JIRA_EMAIL;
const token = process.env.JIRA_API_TOKEN;
if (!base || !email || !token) {
throw new Error(
"Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
);
apps/apollo-vertex/lib/jira-resolve.ts:175
Component:convention parsing only keeps the first token before whitespace, so values likeComponent: Alert DialogorComponent: Button Groupwill slugify toalert/buttonand fail to resolve toalert-dialog/button-group. This breaks the intended convention lookup for multi-word component names.
const componentMatch = descText.match(/^Component:\s*(.+)$/im);
if (componentMatch) {
const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
if (rawSlug) {
const slug = rawSlug
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
const path = SLUG_PATH_MAP.get(slug);
if (path) return { url: path, source: "convention" };
}
apps/apollo-vertex/lib/jira.ts:88
cache: "no-store"forces every page view (and every call to the public API route) to hit Jira directly, which can become a reliability/rate-limit issue. Consider using a short server cache via Next’s fetch revalidation (for example 60s) so the page is still near-real-time but resilient to spikes.
Accept: "application/json",
},
body: JSON.stringify(body),
cache: "no-store",
});
| if (!res.ok) { | ||
| // eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential | ||
| const text = await res.text(); | ||
| throw new Error(`Jira API error ${res.status}: ${text}`); | ||
| } |
655b054 to
9e115a3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (5)
apps/apollo-vertex/app/api/jira/route.ts:19
- This route returns the raw caught error message to unauthenticated callers. Even with improved upstream errors, it’s better to avoid exposing internal failure details on a public endpoint. Also,
Cache-Control: no-storemeans every request will hit Jira, increasing the risk of rate limiting/abuse for a public URL.
return NextResponse.json(data, {
headers: { "Cache-Control": "no-store" },
});
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return NextResponse.json({ error: message }, { status: 500 });
}
apps/apollo-vertex/lib/jira.ts:94
- Throwing
Jira API error ...: ${text}includes the full upstream response body. That error string is later returned to clients (API route) and rendered in the UI (StatusBoard), which can leak internal details and produce very large error payloads. Prefer extracting a short, structured message (or truncating) instead of embedding the full body.
if (!res.ok) {
// eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
const text = await res.text();
throw new Error(`Jira API error ${res.status}: ${text}`);
}
apps/apollo-vertex/lib/jira-resolve.ts:161
- The
Vertex:URL regex will include trailing punctuation (e.g. a period at end of sentence) because the character class allows.and,. That can lead to broken external links for delivered cards.
const vertexMatch = descText.match(
/Vertex(?:\s+URL)?:\s*(https?:\/\/[^\s\n)]+)/i,
);
if (vertexMatch) {
return { url: vertexMatch[1].trim(), source: "explicit" };
}
apps/apollo-vertex/lib/jira-resolve.ts:175
Component:convention slug parsing only takes the first whitespace-delimited token (ButtonfromButton Group), which prevents resolving multi-word slugs likebutton-groupand will incorrectly fall back to Jira for those delivered tickets. Slugify the full component value instead of truncating it.
const componentMatch = descText.match(/^Component:\s*(.+)$/im);
if (componentMatch) {
const rawSlug = componentMatch[1].match(/^([^\s(,]+)/)?.[1];
if (rawSlug) {
const slug = rawSlug
apps/apollo-vertex/app/design-system-status/_components/status-board.tsx:178
- The non-config error state renders the raw error string from Jira/network failures. Since this page is publicly accessible, that can expose internal details (and can be very long if it includes upstream response bodies). Consider showing a generic message to users and keeping the detailed error server-side (logs) or only in development.
) : (
<p className="text-sm text-muted-foreground">{message}</p>
)}
9e115a3 to
7c12a8e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
apps/apollo-vertex/lib/jira-resolve.ts:224
- The comparator returns 0 for most pairs, which relies on the engine’s sort stability to preserve the existing Jira ordering. Stability is not guaranteed across all JS engines/spec interpretations, so the relative order of non-review items can change unexpectedly. Add a deterministic tie-breaker (e.g., compare
updateddescending, thenkey) or decorate with original indices.
function sortComingSoon(cards: ProcessedCard[]): ProcessedCard[] {
return cards.toSorted((a, b) => {
const aReview = isReviewStatus(a.status);
const bReview = isReviewStatus(b.status);
if (aReview !== bReview) return aReview ? -1 : 1;
return 0;
});
}
apps/apollo-vertex/lib/jira.ts:95
- Logging the full Jira error response body can leak sensitive information into server logs (Jira sometimes echoes request context or includes content). Consider truncating/redacting the body and/or only logging verbose bodies in non-production environments; at minimum log status + a request identifier rather than the full payload.
if (!res.ok) {
// eslint-disable-next-line no-await-in-loop -- cursor pagination is inherently sequential
const body = await res.text();
console.error(`Jira API error body (${res.status}):`, body);
throw new Error(`Jira API error: ${res.status} ${res.statusText}`);
}
apps/apollo-vertex/app/design-system-status/_components/status-board.tsx:143
- The UI branches on a substring match of an error message, which tightly couples rendering logic to exact punctuation/wording from the throw site. Prefer throwing a typed error (custom
Errorsubclass) or including a structured error code so the UI can reliably detect missing configuration without parsing strings.
function SetupError({ message }: { message: string }) {
const isMissingConfig = message.includes("Missing Jira configuration");
apps/apollo-vertex/lib/jira.ts:64
- The thrown error directs users to add variables to
.env.local, while the UI error state inStatusBoardinstructsapps/apollo-vertex/.env.local. Align these messages (same path and wording) to avoid confusing setup instructions.
if (!base || !email || !token) {
throw new Error(
"Missing Jira configuration. Add JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN to .env.local",
);
}
apps/apollo-vertex/lib/jira-resolve.ts:5
- This slug list is large and manually inlined, which is likely to drift from the actual site routes over time (despite the comment saying it’s derived from
_meta.ts). To reduce drift, consider generating this map from the source-of-truth route metadata at build time (or exporting/importing a shared constant), or add a validation step/test that asserts the list matches the real_meta.tsentries.
// All known routable slugs per section, derived from the site's _meta.ts files
const SLUGS_BY_SECTION: Record<string, string[]> = {
components: [
| return NextResponse.json(data, { | ||
| headers: { "Cache-Control": "no-store" }, | ||
| }); |
Adds a /design-system-status page (nav: "Roadmap status", positioned directly under Introduction) that reads live from the VS Horizontal UX Jira board and displays three sections: Recently Delivered, Coming Soon, and Backlog. - lib/jira.ts: Jira Cloud REST API v3 client with cursor pagination and ADF text extraction (handles plain text and smartlink inlineCard nodes) - lib/jira-resolve.ts: maps raw issues to ProcessedCard objects, resolves delivered links via explicit Vertex URL > convention slug > Jira fallback, extracts epic from parent field - app/api/jira/route.ts: GET handler returning board data as JSON - app/design-system-status/page.mdx + _components/status-board.tsx: async server component rendering the three-column card board - app/_meta.ts: adds "Roadmap status" nav entry below Introduction Credentials required: JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN in .env.local (local) or Vercel environment variables (production). Board filter: label = "horizontal-ux"; delivered filter: label = "ds-delivered". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7c12a8e to
22ad156
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
apps/apollo-vertex/lib/jira-resolve.ts:224
Array.prototype.toSortedis not available in older Node runtimes (notably Node 18). If this app targets or may run on Node 18 (common for many Next deployments), this will crash at runtime. Consider usingcards.slice().sort(...)(or a small helper) for broader compatibility.
function sortComingSoon(cards: ProcessedCard[]): ProcessedCard[] {
return cards.toSorted((a, b) => {
const aReview = isReviewStatus(a.status);
const bReview = isReviewStatus(b.status);
if (aReview !== bReview) return aReview ? -1 : 1;
return 0;
});
}
apps/apollo-vertex/app/api/jira/route.ts:21
- This endpoint is intentionally public, but returning raw exception messages can leak internal configuration details (e.g., env var names/paths) and upstream error information. Prefer logging the detailed error server-side and returning a generic client-facing message (optionally with a stable error code) to avoid information disclosure.
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return NextResponse.json({ error: message }, { status: 500 });
}
apps/apollo-vertex/app/design-system-status/_components/status-board.tsx:143
- This UI branches on a substring match of the error message, which is brittle (any wording change breaks the detection). A more robust approach is to throw a dedicated error type or attach a stable
code(e.g.,JIRA_CONFIG_MISSING) and branch on that instead of parsingmessage.
function SetupError({ message }: { message: string }) {
const isMissingConfig = message.includes("Missing Jira configuration");
| const parentIsEpic = | ||
| issue.fields.parent?.fields.issuetype.name === "Epic" || | ||
| issue.fields.parent?.fields.issuetype.hierarchyLevel === 1; |
Summary
/design-system-statuspage surfaced as "Roadmap status" in the sidebar, positioned directly under Introductionlabel = "horizontal-ux") and displays three sections: Recently Delivered, Coming Soon, and Backlogds-deliveredJira label appearVertex: <url>in description (supports both plain text and Jira smartlinks) → convention slug lookup → Jira URL fallbackEnvironment variables required
Add to
.env.locallocally and to the Vercel project for production:How to add tickets to the Delivered section
Add the
ds-deliveredlabel to the Jira ticket. No code change needed.How to set the Vertex link on a delivered card
Add
Vertex: https://...as a line in the Jira ticket description.Test plan
/design-system-statusand confirm three sections render with live datads-delivered-labelled ticket(s)Vertex:URL navigates to the Vertex page (not Jira)🤖 Generated with Claude Code